Skip to content

feat: distinct, non-dismissible UI state for TOTP lockout#53

Merged
lakhansamani merged 4 commits into
mainfrom
feat/totp-lockout-ui
Jul 11, 2026
Merged

feat: distinct, non-dismissible UI state for TOTP lockout#53
lakhansamani merged 4 commits into
mainfrom
feat/totp-lockout-ui

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

Summary

  • AuthorizerVerifyOtp now detects errors[0].code === 'TOO_MANY_REQUESTS' on a failed verify_otp and disables the OTP input + Submit button, and stops the error message from being dismissed - previously the lockout showed identically to a plain wrong code, so the form stayed submittable with no signal to stop retrying.
  • No countdown/timer: the backend doesn't expose remaining lockout duration, so this is a static message rather than a timer that could show the wrong number.

Dependency (blocking merge)

This depends on .code being available on authorizer-js errors: authorizerdev/authorizer-js#39 (not yet published to npm). package.json here is intentionally untouched - once that PR is merged and published, bump @authorizerdev/authorizer-js and this will typecheck/build cleanly. Until then errors[0]?.code typechecks against the unpublished SDK shape only (verified locally via npm link), not the currently-pinned 3.2.1.

Test plan

  • tsc --noEmit clean against the linked (unpublished) authorizer-js build
  • Live UAT: ran the example/ app against a local authorizer server (branch security/totp-hardening, includes the extensions.code backend PR), signed up + enrolled TOTP, failed verify_otp 6x through the real browser UI. Confirmed via DOM inspection: input and submit button both disabled: true, banner text "Too many failed attempts, please try again later", no dismiss icon.
  • Not yet re-verified against the published SDK - needs a follow-up once authorizer-js#39 ships and this repo's dependency is bumped.

verify_otp's per-account lockout (5 failed attempts/15min, backend
PR authorizerdev/authorizer#670) returned the same generic dismissible
error as a plain wrong code, so the form stayed submittable and users
had no signal to stop retrying. Key off the new extensions.code
(TOO_MANY_REQUESTS, requires @authorizerdev/authorizer-js with
extensions.code support - authorizerdev/authorizer-js#39) to disable
the OTP input and submit button and keep the message on screen
instead of letting it be dismissed.

No countdown: the backend doesn't expose remaining lockout duration,
so this shows a static message rather than a timer that could be
wrong.
- Hide "Resend OTP" when is_totp is true: TOTP codes are generated
  locally by an authenticator app on a 30s rolling window, never sent
  by the server, so offering to resend was misleading and the backend
  call for it doesn't apply to this flow.
- Add autocomplete="one-time-code" to the OTP input so browsers/OS
  can offer native autofill for authenticator/SMS codes.
@lakhansamani

Copy link
Copy Markdown
Contributor Author

Added two more fixes to this branch (found while auditing this component, kept out of the initial commit until confirmed in scope):

  • Hide "Resend OTP" when is_totp is true — TOTP codes are generated locally, never sent by the server.
  • Added autocomplete="one-time-code" to the OTP input for native browser/OS autofill.

Both verified live: signed in against a local backend with TOTP, confirmed via DOM inspection that the Resend OTP link is absent and the input carries autocomplete="one-time-code".

If the server ever sends TOO_MANY_REQUESTS with an empty message, the
form previously went completely silent: input and Submit both
disabled, no explanation rendered at all (Message returns null for
blank text). Added a fixed fallback string so the user always has
some explanation for why the form just went dead, regardless of what
the server sent.
@lakhansamani

Copy link
Copy Markdown
Contributor Author

Fixed the silent-frozen-form finding: added a fixed fallback message for when the TOO_MANY_REQUESTS error has no (or empty) message text, so the disabled form always has an explanation.

@lakhansamani

Copy link
Copy Markdown
Contributor Author

Blocked on an authorizer-js release. This uses errors[0]?.code === 'TOO_MANY_REQUESTS', and the .code field on SDK errors ships in authorizer-js (merged, not yet published to npm). CI fails on published 3.2.1 (Property 'code' does not exist on type 'Error'). Unblock: publish authorizer-js, bump the @authorizerdev/authorizer-js dep here, then rebase (this file was just changed by #55 — resolve the onSubmit/state overlap) and CI will go green.

@lakhansamani lakhansamani merged commit 36558b6 into main Jul 11, 2026
4 of 5 checks passed
@lakhansamani lakhansamani deleted the feat/totp-lockout-ui branch July 11, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant